﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using SimpleData.ObjectModel;
using Microsoft.Ajax.Utilities;
using LinqExpression = System.Linq.Expressions.Expression;
using System.Dynamic;
using System.Reflection;

namespace SimpleData.Infrastruture.JavaScript
{

	/// <summary>
	/// Representa um schema escrito em JavaScript
	/// </summary>
	public sealed class JavaScriptDocumentSchema : DocumentSchema
	{
		private Lazy<Block> block ;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="qualifiedName"></param>
		/// <param name="source"></param>
		public JavaScriptDocumentSchema( DataObjectName qualifiedName, string source )
			: base( qualifiedName )
		{
			this.Source = source;
			this.block = new Lazy<Block>( this.ParseSource );
		}

		/// <summary>
		/// Obtém ou define o código utilizado para identificar o schema
		/// </summary>
		public string Source
		{ get; private set; }

		/// <summary>
		/// Obtém a expressão JavaScript com o schema do document
		/// </summary>
		internal protected Block JavaScriptBlock
		{
			get { return this.block.Value; }
		}


		protected override IList<Field> CreateListField()
		{
			var fields = ( from f in this.JavaScriptBlock.GetFields()
						   select new Field( new DataObjectName( this.QualifiedName.Namespace, Convert.ToString( f.Value ) ) ) );
			return fields.ToList();
			//return this.JavaScriptBlock.GetFields();
		}

		public override DynamicMetaObject CreateMetaObject( LinqExpression parameter, Document doc )
		{
			if ( !this.QualifiedName.Equals( doc.Schema.QualifiedName ) )
				throw new InvalidOperationException();

			return new JavaScriptMetaObject( parameter, doc, this );
		}

		private Block ParseSource()
		{
			JSParser parser = new JSParser( this.Source );
			var settings = parser.Settings;

			settings.CollapseToLiteral = true;
			settings.EvalLiteralExpressions = true;
			settings.MinifyCode = false;
			settings.PreserveFunctionNames = true;
			settings.LocalRenaming = LocalRenaming.KeepAll;

			return parser.Parse( settings );
		}
	}


	internal sealed class JavaScriptMetaObject : DocumentSchemaMetaObject
	{

		public JavaScriptMetaObject( LinqExpression parameter, Document doc, JavaScriptDocumentSchema schema )
			: base( parameter, doc )
		{
			this.Schema = schema;
			//this.fields = new Lazy<Dictionary<string, ObjectLiteralField>>( this.ExtractFields, false );
		}

		public JavaScriptDocumentSchema Schema
		{ get; private set; }


		public override DynamicMetaObject BindGetMember( GetMemberBinder binder )
		{
			Field field = ( from f in this.Schema.Fields
							where f.QualifiedName.NameEquals( binder.Name )
							select f ).FirstOrDefault();

			if ( field != null )
			{
				var self = base.Expression;
				var name = field.QualifiedName;

				// Expressáo utilizada para obter o valor do campo "field"
				Expression<Func<Document, object>> getter = 
					doc => doc.Values.GetValueOrDefault( name );

				// Remove a expressáo lambda e altera a variável "doc" para "this"
				LinqExpression getValue = getter.RemoveLambda().Replace( getter.Parameters[ 0 ], self );

				BindingRestrictions restrictions = BindingRestrictions.GetInstanceRestriction( self, ( Document )this.Value );
				return new DynamicMetaObject( LinqExpression.Convert( getValue, field.Type.FrameworkType ), restrictions );
			}
			else
			{
				return base.BindGetMember( binder );
			}
		}

		public override DynamicMetaObject BindSetMember( SetMemberBinder binder, DynamicMetaObject value )
		{
			Field field = ( from f in this.Schema.Fields
							where f.QualifiedName.NameEquals( binder.Name )
							select f ).FirstOrDefault();

			if ( field != null )
			{
				var self = base.Expression;
				var fieldName = field.QualifiedName;
				var docValuesType = typeof( DocumentValueCollection);

				// Expressáo utilizada para armazenar o valor
				LinqExpression setter = LinqExpression.MakeMemberAccess( self, base.Expression.Type.GetProperty( "Values" ) );
				setter = LinqExpression.MakeIndex( setter, docValuesType.GetIndexProperty(), new LinqExpression[] { LinqExpression.Constant( fieldName ) } );
				setter = LinqExpression.Assign( setter, value.Expression );

				BindingRestrictions restrictions = BindingRestrictions.GetInstanceRestriction( self, ( Document )this.Value );
				return new DynamicMetaObject( setter, restrictions );
			}
			else
				return base.BindSetMember( binder, value );
		}

		//private IList<ObjectLiteralField> ExtractFields()
		//{
		//    JsMemberExtractor extractor = new JsMemberExtractor();
		//    this.Schema.JavaScriptBlock.Accept( extractor );

		//    return extractor.Fields;
		//}

		public override IEnumerable<string> GetDynamicMemberNames()
		{
			return this.Schema.Fields.Select( field => field.QualifiedName.Name );
		}

	}


	internal sealed class JsMemberExtractor : TreeVisitor
	{

		public JsMemberExtractor()
		{
			this.Fields = new List<ObjectLiteralField>();
		}

		public List<ObjectLiteralField> Fields
		{
			get;
			private set;
		}

		public override void Visit( ArrayLiteral node )
		{
			base.Visit( node );
		}

		public override void Visit( ThisLiteral node )
		{
			base.Visit( node );
		}

		public override void Visit( GetterSetter node )
		{
			base.Visit( node );
		}

		public override void Visit( ObjectLiteral node )
		{
			foreach ( var field in node.Keys )
				this.Fields.Add( field );
			//base.Visit( node );
		}

		public override void Visit( ObjectLiteralField node )
		{
			//base.Visit( node );
		}
	}

	internal static class JsBlockExtensions
	{
		public static IList<ObjectLiteralField> GetFields( this Block block )
		{
			JsMemberExtractor members = new JsMemberExtractor();
			block.Accept( members );
			return members.Fields;
		}
	}

	internal static class JavaScriptDocumentSchemaExtensions
	{
		public static PropertyInfo GetIndexProperty(this Type type)
		{
			return null;
		}
	}
}
